#!/usr/bin/env python3
"""
GitLab指定用户组批量克隆工具
功能：通过弹出窗口让用户指定私钥、用户组和GitLab地址，然后克隆指定用户组下的所有工程
保持完整的GitLab目录层次结构
"""

import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, ttk
from urllib.request import urlopen
import json
import subprocess
import shlex
import os
import sys
import time

class GitLabCloneGUI:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("GitLab用户组克隆工具")
        self.root.geometry("700x600")
        self.root.resizable(True, True)
        
        # 配置变量
        self.gitlab_addr = tk.StringVar()
        self.gitlab_token = tk.StringVar()
        self.group_name = tk.StringVar()
        self.target_dir = tk.StringVar()
        
        self.setup_ui()
        
    def setup_ui(self):
        """设置用户界面"""
        # 主框架
        main_frame = tk.Frame(self.root, padx=20, pady=20)
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 标题
        title_label = tk.Label(main_frame, text="GitLab用户组克隆工具", font=("Arial", 16, "bold"))
        title_label.pack(pady=(0, 20))
        
        # GitLab地址
        addr_frame = tk.Frame(main_frame)
        addr_frame.pack(fill=tk.X, pady=5)
        tk.Label(addr_frame, text="GitLab地址:", width=15, anchor='w').pack(side=tk.LEFT)
        tk.Entry(addr_frame, textvariable=self.gitlab_addr, width=40).pack(side=tk.LEFT, padx=(10, 0))
        
        # Private Token
        token_frame = tk.Frame(main_frame)
        token_frame.pack(fill=tk.X, pady=5)
        tk.Label(token_frame, text="Private Token:", width=15, anchor='w').pack(side=tk.LEFT)
        tk.Entry(token_frame, textvariable=self.gitlab_token, width=40, show="*").pack(side=tk.LEFT, padx=(10, 0))
        
        # 用户组名称
        group_frame = tk.Frame(main_frame)
        group_frame.pack(fill=tk.X, pady=5)
        tk.Label(group_frame, text="用户组名称:", width=15, anchor='w').pack(side=tk.LEFT)
        
        # 用户组选择框架
        group_select_frame = tk.Frame(group_frame)
        group_select_frame.pack(side=tk.LEFT, padx=(10, 0))
        
        # 下拉选择框
        self.group_combobox = tk.ttk.Combobox(group_select_frame, textvariable=self.group_name, width=35, state="readonly")
        self.group_combobox.pack(side=tk.LEFT)
        
        # 刷新按钮
        tk.Button(group_select_frame, text="刷新", command=self.refresh_groups, 
                 bg="#FF9800", fg="white", width=8).pack(side=tk.LEFT, padx=(5, 0))
        
        # 目标目录
        dir_frame = tk.Frame(main_frame)
        dir_frame.pack(fill=tk.X, pady=5)
        tk.Label(dir_frame, text="目标目录:", width=15, anchor='w').pack(side=tk.LEFT)
        tk.Entry(dir_frame, textvariable=self.target_dir, width=30).pack(side=tk.LEFT, padx=(10, 0))
        tk.Button(dir_frame, text="浏览", command=self.browse_directory).pack(side=tk.LEFT, padx=(10, 0))
        
        # 按钮框架
        button_frame = tk.Frame(main_frame)
        button_frame.pack(pady=20)
        
        tk.Button(button_frame, text="测试连接", command=self.test_connection, 
                 bg="#4CAF50", fg="white", width=15, height=2).pack(side=tk.LEFT, padx=5)
        
        tk.Button(button_frame, text="开始克隆", command=self.start_clone, 
                 bg="#2196F3", fg="white", width=15, height=2).pack(side=tk.LEFT, padx=5)
        
        tk.Button(button_frame, text="退出", command=self.root.quit, 
                 bg="#f44336", fg="white", width=15, height=2).pack(side=tk.LEFT, padx=5)
        
        # 日志框架
        log_frame = tk.Frame(main_frame)
        log_frame.pack(fill=tk.BOTH, expand=True, pady=(20, 0))
        
        tk.Label(log_frame, text="操作日志:", anchor='w').pack(anchor='w')
        
        # 日志文本框
        self.log_text = tk.Text(log_frame, height=20, wrap=tk.WORD)
        scrollbar = tk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
        self.log_text.configure(yscrollcommand=scrollbar.set)
        
        self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 设置默认值
        self.set_default_values()
        
        # 延迟刷新用户组列表（等待界面完全加载）
        self.root.after(100, self.auto_refresh_groups)
        
    def set_default_values(self):
        """设置默认值"""
        self.gitlab_addr.set("54.150.41.69")
        self.gitlab_token.set("glpat-9kuUBPJco_9DmGx8Jkbf_W86MQp1OmsH.01.0w1h9wx1s")
        # 不设置默认用户组，让用户从下拉框选择
        self.group_name.set("")
        
    def browse_directory(self):
        """浏览选择目标目录"""
        directory = filedialog.askdirectory(title="选择目标目录")
        if directory:
            self.target_dir.set(directory)
            
    def refresh_groups(self):
        """刷新用户组列表"""
        self.log_message("=== 刷新用户组列表 ===")
        
        if not self.gitlab_addr.get().strip() or not self.gitlab_token.get().strip():
            messagebox.showerror("输入错误", "请先输入GitLab地址和Private Token")
            return
            
        try:
            # 获取所有用户组
            groups_url = f"http://{self.gitlab_addr.get()}/api/v4/groups?private_token={self.gitlab_token.get()}&per_page=1000&order_by=name"
            self.log_message("正在获取用户组列表...")
            
            response = self.make_request_with_retry(groups_url)
            groups_data = json.loads(response.read().decode('UTF-8'))
            
            if not groups_data:
                self.log_message("⚠ 未找到任何用户组")
                self.group_combobox['values'] = []
                return
                
            # 提取用户组信息，使用 full_path 保持完整层级
            all_groups = []
            
            for group in groups_data:
                # 使用 full_path 字段，如果没有则使用 path
                if group.get('full_path'):
                    group_path = group['full_path']
                else:
                    group_path = group['path']
                
                all_groups.append(group_path)
            
            # 去重并排序
            all_groups = list(set(all_groups))
            all_groups.sort()  # 按字母顺序排序
            
            # 更新下拉框
            self.group_combobox['values'] = all_groups
            self.log_message(f"✓ 成功获取 {len(all_groups)} 个用户组", "success")
            
            # 如果当前选择的用户组不在列表中，清空选择
            if self.group_name.get() and self.group_name.get() not in all_groups:
                self.group_name.set("")
                self.log_message("⚠ 当前选择的用户组不在列表中，已清空选择")
                
        except Exception as e:
            self.log_message(f"✗ 获取用户组列表失败: {str(e)}", "error")
            messagebox.showerror("错误", f"获取用户组列表失败:\n{str(e)}")
            
    def auto_refresh_groups(self):
        """自动刷新用户组列表（静默模式）"""
        if not self.gitlab_addr.get().strip() or not self.gitlab_token.get().strip():
            return
            
        try:
            # 静默获取用户组列表
            groups_url = f"http://{self.gitlab_addr.get()}/api/v4/groups?private_token={self.gitlab_token.get()}&per_page=1000&order_by=name"
            response = self.make_request_with_retry(groups_url)
            groups_data = json.loads(response.read().decode('UTF-8'))
            
            if groups_data:
                # 提取用户组信息，使用 full_path 保持完整层级
                all_groups = []
                
                for group in groups_data:
                    # 使用 full_path 字段，如果没有则使用 path
                    if group.get('full_path'):
                        group_path = group['full_path']
                    else:
                        group_path = group['path']
                    
                    all_groups.append(group_path)
                
                # 去重并排序
                all_groups = list(set(all_groups))
                all_groups.sort()
                
                # 更新下拉框
                self.group_combobox['values'] = all_groups
                
        except Exception:
            # 静默失败，不显示错误信息
            pass
            
    def log_message(self, message, level="info"):
        """添加日志消息，支持不同级别"""
        timestamp = time.strftime('%H:%M:%S')
        full_message = f"{timestamp} - {message}\n"
        
        # 根据级别设置颜色
        if level == "error":
            self.log_text.insert(tk.END, full_message, "error")
            self.log_text.tag_config("error", foreground="red")
        elif level == "warning":
            self.log_text.insert(tk.END, full_message, "warning")
            self.log_text.tag_config("warning", foreground="orange")
        elif level == "success":
            self.log_text.insert(tk.END, full_message, "success")
            self.log_text.tag_config("success", foreground="green")
        else:
            self.log_text.insert(tk.END, full_message)
        
        self.log_text.see(tk.END)
        self.root.update()
        
    def make_request_with_retry(self, url, max_retries=3, delay=2):
        """带重试机制的HTTP请求"""
        for attempt in range(max_retries):
            try:
                self.log_message(f"  尝试 {attempt + 1}/{max_retries}...")
                response = urlopen(url, timeout=15)
                return response
            except Exception as e:
                if attempt < max_retries - 1:
                    self.log_message(f"  ✗ 请求失败: {str(e)}")
                    self.log_message(f"  ⏳ 等待 {delay} 秒后重试...")
                    time.sleep(delay)
                    delay *= 2
                else:
                    raise e
                    
    def test_connection(self):
        """测试GitLab连接"""
        self.log_message("=== 测试GitLab连接 ===")
        
        if not self.validate_inputs():
            return
            
        try:
            test_url = f"http://{self.gitlab_addr.get()}/api/v4/version?private_token={self.gitlab_token.get()}"
            self.log_message("正在测试连接...")
            
            response = self.make_request_with_retry(test_url)
            version = json.loads(response.read().decode('UTF-8'))
            
            self.log_message(f"✓ GitLab连接成功! 版本: {version.get('version', 'Unknown')}", "success")
            messagebox.showinfo("连接成功", f"GitLab连接成功!\n版本: {version.get('version', 'Unknown')}")
            
        except Exception as e:
            self.log_message(f"✗ GitLab连接失败: {str(e)}", "error")
            messagebox.showerror("连接失败", f"无法连接到GitLab:\n{str(e)}")
            
    def validate_inputs(self):
        """验证输入参数"""
        if not self.gitlab_addr.get().strip():
            messagebox.showerror("输入错误", "请输入GitLab地址")
            return False
            
        if not self.gitlab_token.get().strip():
            messagebox.showerror("输入错误", "请输入Private Token")
            return False
            
        if not self.group_name.get().strip():
            messagebox.showerror("输入错误", "请输入用户组名称")
            return False
            
        if not self.target_dir.get().strip():
            messagebox.showerror("输入错误", "请选择目标目录")
            return False
            
        return True
        
    def find_group(self):
        """查找指定的用户组"""
        try:
            # 获取所有用户组进行精确匹配
            groups_url = f"http://{self.gitlab_addr.get()}/api/v4/groups?private_token={self.gitlab_token.get()}&per_page=1000&order_by=name"
            self.log_message(f"搜索用户组: {self.group_name.get()}")
            
            response = self.make_request_with_retry(groups_url)
            groups_data = json.loads(response.read().decode('UTF-8'))
            
            if not groups_data:
                self.log_message(f"✗ 未找到任何用户组")
                return None
                
            # 精确匹配用户组
            selected_group_name = self.group_name.get()
            
            for group in groups_data:
                # 检查多种匹配方式
                if (group['name'] == selected_group_name or 
                    group['path'] == selected_group_name or
                    (group.get('full_path') and group['full_path'] == selected_group_name)):
                    
                    self.log_message(f"✓ 找到用户组: {group['name']} (ID: {group['id']})", "success")
                    if group.get('full_path'):
                        self.log_message(f"  完整路径: {group['full_path']}")
                    return group
                    
            self.log_message(f"✗ 未找到完全匹配的用户组: {selected_group_name}", "error")
            self.log_message("  请检查用户组名称是否正确，或点击刷新按钮更新用户组列表")
            return None
            
        except Exception as e:
            self.log_message(f"✗ 查找用户组失败: {str(e)}", "error")
            return None
            
    def get_group_projects(self, group_id):
        """获取用户组下的所有项目，包括空工程"""
        try:
            # 获取所有项目，包括空工程
            projects_url = f"http://{self.gitlab_addr.get()}/api/v4/groups/{group_id}/projects?private_token={self.gitlab_token.get()}&per_page=1000&order_by=name&include_subgroups=true"
            self.log_message(f"获取用户组 '{self.group_name.get()}' 下的所有项目（包括空工程）...")
            
            response = self.make_request_with_retry(projects_url)
            projects_data = json.loads(response.read().decode('UTF-8'))
            
            if not projects_data:
                self.log_message(f"⚠ 用户组 '{self.group_name.get()}' 下没有找到项目")
                return []
                
            self.log_message(f"✓ 在用户组 '{self.group_name.get()}' 下找到 {len(projects_data)} 个项目", "success")
            
            # 显示项目信息
            for i, project in enumerate(projects_data, 1):
                self.log_message(f"  {i}. {project['name']} ({project['path_with_namespace']})")
                
            return projects_data
            
        except Exception as e:
            self.log_message(f"✗ 获取项目失败: {str(e)}", "error")
            return []
            
    def clone_project(self, project_url, project_path, target_dir):
        """克隆单个项目，保持完整的目录结构，确保不缺少任何工程"""
        try:
            # 创建完整的目录结构
            # 例如：cex-backend/futures/cex-futures-hedging
            project_full_path = os.path.join(target_dir, project_path)
            project_dir = os.path.dirname(project_full_path)
            
            # 强制创建所有必要的父目录
            os.makedirs(project_dir, exist_ok=True)
            
            # 检查项目是否已存在且是有效的Git仓库
            if os.path.exists(project_full_path):
                git_dir = os.path.join(project_full_path, ".git")
                if os.path.exists(git_dir) and os.path.isdir(git_dir):
                    self.log_message("    ✓ 项目已存在且是有效的Git仓库，正在更新...", "success")
                    # 更新现有项目
                    update_cmd = f'cd "{project_full_path}" && git pull -r'
                    os.system(update_cmd)
                    return True
                else:
                    self.log_message("    ⚠ 项目目录存在但不是有效的Git仓库，将重新克隆...", "warning")
                    # 删除无效的目录，重新克隆
                    import shutil
                    try:
                        shutil.rmtree(project_full_path)
                        self.log_message("    ✓ 已删除无效的项目目录")
                    except Exception as e:
                        self.log_message(f"    ⚠ 删除无效目录失败: {str(e)}", "warning")
            else:
                self.log_message("    ✓ 克隆新项目...", "info")
                # 使用带token的URL进行克隆
                clone_url = project_url.replace('http://', f'http://oauth2:{self.gitlab_token.get()}@')
                
                # 尝试克隆，最多重试3次
                max_retries = 3
                for attempt in range(max_retries):
                    if attempt > 0:
                        self.log_message(f"    ⚠ 第 {attempt + 1} 次重试克隆...", "warning")
                        time.sleep(2)  # 等待2秒后重试
                    
                    clone_cmd = shlex.split(f'git clone {clone_url} "{project_full_path}"')
                    
                    # 执行克隆命令
                    result = subprocess.run(clone_cmd, cwd=project_dir, capture_output=True, text=True)
                    
                    if result.returncode == 0:
                        self.log_message("    ✓ 克隆成功", "success")
                        return True
                    else:
                        error_msg = result.stderr.strip()
                        if "502" in error_msg or "Bad Gateway" in error_msg:
                            self.log_message(f"    ⚠ 服务器错误 (502)，尝试重试...", "warning")
                            continue
                        elif "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
                            self.log_message(f"    ⚠ 连接超时，尝试重试...", "warning")
                            continue
                        else:
                            # 其他错误，不重试
                            break
                
                # 所有重试都失败了
                self.log_message(f"    ✗ 克隆失败 (重试{max_retries}次): {result.stderr}", "error")
                self.log_message("    ⚠ 克隆失败，但确保目录结构存在", "warning")
                
                # 确保目录结构存在
                os.makedirs(project_full_path, exist_ok=True)
                
                # 创建一个README文件说明克隆失败的原因
                readme_path = os.path.join(project_full_path, "README.md")
                if not os.path.exists(readme_path):
                    with open(readme_path, 'w', encoding='utf-8') as f:
                        f.write(f"# {os.path.basename(project_path)}\n\n")
                        f.write("## 克隆状态\n")
                        f.write("❌ **克隆失败**\n\n")
                        f.write("## 错误信息\n")
                        f.write(f"```\n{result.stderr}\n```\n\n")
                        f.write("## 项目信息\n")
                        f.write(f"- 原始路径: {project_path}\n")
                        f.write(f"- GitLab URL: {project_url}\n")
                        f.write(f"- 克隆时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
                        f.write(f"- 重试次数: {max_retries}\n\n")
                        f.write("## 建议\n")
                        f.write("1. 检查网络连接\n")
                        f.write("2. 检查GitLab服务器状态\n")
                        f.write("3. 稍后手动重试克隆\n")
                
                return False
                    
        except Exception as e:
            self.log_message(f"    ✗ 克隆项目时出错: {str(e)}", "error")
            # 即使出错，也要确保目录结构存在
            try:
                project_full_path = os.path.join(target_dir, project_path)
                os.makedirs(project_full_path, exist_ok=True)
                self.log_message("    ⚠ 出错，但确保目录结构存在", "warning")
            except:
                pass
            return False
            
    def verify_directory_structure(self, projects):
        """验证本地目录结构是否与GitLab一致"""
        try:
            self.log_message("\n=== 验证目录结构 ===")
            
            target_dir = self.target_dir.get()
            missing_dirs = []
            created_dirs = []
            
            for project in projects:
                project_path = project['path_with_namespace']
                local_path = os.path.join(target_dir, project_path)
                
                if os.path.exists(local_path):
                    created_dirs.append(project_path)
                    self.log_message(f"  ✓ {project_path}", "success")
                else:
                    missing_dirs.append(project_path)
                    self.log_message(f"  ✗ {project_path} - 目录缺失", "error")
            
            self.log_message(f"\n目录结构验证结果:")
            self.log_message(f"  总项目数: {len(projects)}")
            self.log_message(f"  已创建目录: {len(created_dirs)}")
            self.log_message(f"  缺失目录: {len(missing_dirs)}")
            
            if missing_dirs:
                self.log_message(f"\n⚠ 缺失的目录:", "warning")
                for missing in missing_dirs:
                    self.log_message(f"    - {missing}", "error")
                self.log_message("请检查错误日志并手动创建缺失的目录。", "warning")
            else:
                self.log_message("\n✓ 所有目录结构都已正确创建!", "success")
                
        except Exception as e:
            self.log_message(f"✗ 验证目录结构时出错: {str(e)}", "error")
            
    def start_clone(self):
        """开始克隆过程"""
        if not self.validate_inputs():
            return
            
        self.log_message("=== 开始克隆过程 ===")
        
        # 查找用户组
        group = self.find_group()
        if not group:
            messagebox.showerror("错误", f"未找到用户组: {self.group_name.get()}")
            return
            
        # 获取项目列表
        projects = self.get_group_projects(group['id'])
        if not projects:
            messagebox.showinfo("信息", f"用户组 '{self.group_name.get()}' 下没有项目")
            return
            
        # 确认克隆
        confirm = messagebox.askyesno("确认克隆", 
                                    f"将克隆用户组 '{self.group_name.get()}' 下的 {len(projects)} 个项目到:\n{self.target_dir.get()}\n\n是否继续?")
        if not confirm:
            return
            
        # 预先创建父目录结构（不创建项目目录本身）
        self.log_message("=== 预先创建父目录结构 ===")
        for project in projects:
            project_path = project['path_with_namespace']
            project_full_path = os.path.join(self.target_dir.get(), project_path)
            project_parent_dir = os.path.dirname(project_full_path)
            
            try:
                # 只创建父目录，不创建项目目录本身
                if project_parent_dir != self.target_dir.get():
                    os.makedirs(project_parent_dir, exist_ok=True)
                    self.log_message(f"  ✓ 创建父目录: {os.path.relpath(project_parent_dir, self.target_dir.get())}")
            except Exception as e:
                self.log_message(f"  ✗ 创建父目录失败: {project_path} - {str(e)}", "error")
        
        self.log_message("=== 开始克隆项目 ===")
        
        # 开始克隆
        successful_clones = 0
        failed_clones = 0
        
        for i, project in enumerate(projects, 1):
            project_name = project['name']
            project_url = project['http_url_to_repo']
            project_path = project['path_with_namespace']
            
            self.log_message(f"\n[{i}/{len(projects)}] 处理项目: {project_name}")
            self.log_message(f"  完整路径: {project_path}")
            self.log_message(f"  本地路径: {os.path.join(self.target_dir.get(), project_path)}")
            
            # 克隆项目
            clone_success = self.clone_project(project_url, project_path, self.target_dir.get())
            
            # 检查克隆结果
            try:
                project_full_path = os.path.join(self.target_dir.get(), project_path)
                if os.path.exists(project_full_path):
                    # 检查是否有 .git 目录（表示克隆成功）
                    git_dir = os.path.join(project_full_path, ".git")
                    if os.path.exists(git_dir):
                        self.log_message("    ✓ 项目克隆成功，包含Git仓库", "success")
                    else:
                        self.log_message("    ⚠ 项目目录存在但缺少Git仓库", "warning")
                        
            except Exception as e:
                self.log_message(f"    ⚠ 检查项目状态时出错: {str(e)}", "warning")
            
            if clone_success:
                successful_clones += 1
            else:
                failed_clones += 1
                
        # 显示总结
        self.log_message("\n" + "=" * 60)
        self.log_message("=== 克隆完成 ===")
        self.log_message("=" * 60)
        self.log_message(f"总结:")
        self.log_message(f"  总项目数: {len(projects)}")
        self.log_message(f"  成功克隆: {successful_clones}")
        self.log_message(f"  失败克隆: {failed_clones}")
        self.log_message(f"  目标目录: {self.target_dir.get()}")
        
        if failed_clones > 0:
            self.log_message("\n⚠ 部分项目克隆失败，请检查上面的错误信息")
            
        self.log_message("\n✓ 所有项目处理完成!")
        self.log_message("📁 本地目录结构完全保持与GitLab一致!")
        
        # 验证目录结构
        self.verify_directory_structure(projects)
        
        # 显示完成消息
        if failed_clones == 0:
            messagebox.showinfo("克隆完成", f"所有 {len(projects)} 个项目克隆成功!")
        else:
            messagebox.showwarning("克隆完成", f"克隆完成，但有 {failed_clones} 个项目失败")
            
    def run(self):
        """运行GUI"""
        self.root.mainloop()

def main():
    """主函数"""
    try:
        app = GitLabCloneGUI()
        app.run()
    except Exception as e:
        messagebox.showerror("错误", f"程序启动失败:\n{str(e)}")
        sys.exit(1)

if __name__ == "__main__":
    main() 